/**
 * Filename Server.java
 */

package moodish.server;

import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;

import moodish.dummy.ServerCommDummy;
import moodish.comm.ServerMessage;
import moodish.comm.ServerSideMessage;

/**
 * This class implements the {@link moodish.server.MoodishServer} interface and
 * is responsible for:
 * <ul>
 * <li>maintaining a list of the current online clients
 * <li>maintaining and updating a list of who is friend with whom
 * <li>relaying moodish messages between clients
 * <li>handling friendship / unfriendship requests between clients
 * <li>informing clients on online / offline status
 * </ul>
 * 
 * @see moodish.client
 * @see moodish.comm
 * @see moodish.server.MoodishServer
 * @see java.lang.Exception
 * 
 * @author Joao Maria, Tiago, Paulo, Catarina
 * @version 1.0 11/Dec/2013
 * 
 */
public class Server extends Exception implements MoodishServer {

	private static final long serialVersionUID = 1L;

	/** Control boolean. */
	private boolean stopped = true;

	/** Internal server information */
	private int errors = 0, onlineUsers = 0, processedMessages = 0;

	/** The list containing the information on all connected clients */
	private LinkedList<Client> clientList = new LinkedList<Client>();

	/** A list containing the names of all currently online clients */
	private LinkedList<String> onlineClients = new LinkedList<String>();

	/** The error log. */
	private LinkedList<ErrorLog> errorLog = new LinkedList<ErrorLog>();

	/** The server comm buffer */
	private ServerCommDummy serverCommDummy;

	/** These constants are used internally for message processing */
	private static final int MESSAGE_TYPE_MOODISH = 0;
	private static final int MESSAGE_TYPE_CONNECT = 1;
	private static final int MESSAGE_TYPE_DISCONNECT = 2;
	private static final int MESSAGE_TYPE_FRIENDSHIP = 3;
	private static final int MESSAGE_TYPE_UNFRIENDSHIP = 4;

	/*
	 * (non-Javadoc)
	 * 
	 * @see moodish.server.MoodishServer#start(moodish.comm.serverCommDummy)
	 */
	@Override
	public void start(ServerCommDummy inserverCommDummy) {
		inserverCommDummy.start();
		assert (inserverCommDummy != null);

		if (stopped) {

			stopped = false;

			// Initiate the serverCommDummy
			serverCommDummy = inserverCommDummy;

			while (!stopped) {

				// Attempts to retrieve the next message, if any - will hold
				// until the buffer has a message
				ServerMessage message = serverCommDummy.getNextMessage();
				try {
					if (message.getType() == ServerSideMessage.Type.MOODISH_MESSAGE) {
						handleMoodishMessage(message);
					} else if (message.getType() == ServerSideMessage.Type.CLIENT_CONNECTED) {
						connectUser(message.getClientNickname());
					} else if (message.getType() == ServerSideMessage.Type.CLIENT_DISCONNECTED) {
						disconnectUser(message.getClientNickname());
					} else if (message.getType() == ServerSideMessage.Type.FRIENDSHIP) {
						handleFriendshipMessage(message);
					} else if (message.getType() == ServerSideMessage.Type.UNFRIENDSHIP) {
						handleUnriendshipMessage(message);
					} else {
						logError("Could not interpret message type",
								Thread.currentThread().getStackTrace()[1]
										.getMethodName());
					}
				} catch (Exception e) {
					logError(e.getStackTrace(), Thread.currentThread()
							.getStackTrace()[1].getMethodName());
				}
			}
		} else {
			logError("Server startup failure", "root");
			printInternalData(true);
			throw new Error("Server startup failure!");
		}
		return;
	}

	/**
	 * This will test the server and its functions but will not start the server
	 * itself.
	 */
	/**
	 * Prints the internal server data.
	 */
	public void printInternalData() {
		System.out.println(nameListOutput()
				+ "--------\nInternal data this cycle: " + errors + " errors, "
				+ onlineUsers + " users online, " + processedMessages
				+ " messages received and/or sent.\n");
	}

	/**
	 * Provides internal data as a String.
	 * 
	 * @return the internal data as a string
	 */
	public String internalData() {
		return nameListOutput() + "--------\nInternal data this cycle: "
				+ errors + " errors, " + onlineUsers + " users online, "
				+ processedMessages + " messages received and/or sent.\n";
	}

	/**
	 * Prints the internal data.
	 * 
	 * @param errorsToo
	 *            the errors too
	 */
	public void printInternalData(boolean errorsToo) {
		if (errorsToo) {

			printInternalData();
			System.out.println("NO SERVER" + printErrorLog());

		} else
			printInternalData();
	}

	/**
	 * Internal data.
	 * 
	 * @param errorsToo
	 *            the errors too
	 * @return the string with the internal data and the errors
	 */
	public String internalData(boolean errorsToo) {
		String output;

		if (errorsToo)
			output = internalData() + printErrorLog();
		else
			output = internalData();

		return output;
	}

	/**
	 * Prints the error log.
	 * 
	 * @return the string with the error log
	 */
	public String printErrorLog() {

		String output = "Moodish Server Error log:\n";

		Iterator<ErrorLog> iterator = errorLog.iterator();

		boolean ended = !iterator.hasNext();

		int counter = 0;

		while (!ended) {
			counter++;
			output = output + counter + ":" + iterator.next().detailOutput()
					+ "\n";

			if (!iterator.hasNext())
				ended = true;
		}
		return output;
	}

	/**
	 * Prints the error log and then clears it
	 * 
	 * @param clear
	 *            the clear flag
	 */
	public void printErrorLog(boolean clear) {
		if (clear) {
			printErrorLog();
			clearErrorLog();
		} else
			printErrorLog();
	}

	/**
	 * Logs an error in the error log
	 * 
	 * @param error
	 *            the error details as a string
	 * @param method
	 *            the method
	 */
	private void logError(String error, String method) {
		errors++;
		ErrorLog newError = new ErrorLog(error, method);

		errorLog.add(newError);
	}

	/**
	 * Log error in the error log
	 * 
	 * @param stack
	 *            the stack trace of the thrown exception
	 * @param method
	 *            the method
	 */
	private void logError(StackTraceElement[] stack, String method) {
		errors++;

		String output = "";

		for (int i = 1; i < stack.length; i++) {
			output = output + stack[i] + "\n";
		}

		ErrorLog newError = new ErrorLog(output, method);

		errorLog.add(newError);
	}

	/**
	 * Clears error log.
	 */
	private void clearErrorLog() {
		errors = 0;
		errorLog = new LinkedList<ErrorLog>();
	}

	/**
	 * Connect {@link Client}.
	 * 
	 * @param nickname
	 *            the nickname
	 * 
	 * 
	 */
	private void connectUser(String nickname) {
		assert nickname != null;

		try {
			processedMessages++;
			if (clientExists(nickname)) {
				if (getClientByNickname(nickname).connected) {
					errors++;
				} else
					connectExistingUser(nickname);
			} else
				connectNewUser(nickname);
		} catch (Exception e) {
			logError(e.getStackTrace(),
					Thread.currentThread().getStackTrace()[1].getMethodName());
		}
	}

	/**
	 * Connect a new {@link Client}
	 * 
	 * @param nickname
	 *            the nickname of the new client
	 * @throws Exception
	 *             When a {@link Server.Client} name already exists in the
	 *             database. We do not like duplicates.
	 */
	private void connectNewUser(String nickname) throws Exception {
		if (getClientByNickname(nickname) == null) {

			Client newClient = new Client(nickname);
			newClient.connected = true;
			clientList.add(newClient);
			onlineClients.add(nickname);
			onlineUsers++;
			floodMessageProcessor(nickname, MESSAGE_TYPE_CONNECT);
		} else {

			throw new RuntimeException(
					"Clientname already exists in server database");
		}
	}

	/**
	 * If by some reason the client is not deleted from the database, and is
	 * currently offline, this method will connect him and will alow him to
	 * remain in the server
	 * 
	 * 
	 * 
	 * @param nickname
	 *            the nickname
	 * @throws Exception
	 *             If the {@link Server.Client} is not found.
	 */
	private void connectExistingUser(String nickname) throws Exception {

		if (clientExists(nickname)) {

			clientList.get(getClientIndexByNickname(nickname)).connected = true;
			onlineClients.add(nickname);
			onlineUsers++;

			floodMessageProcessor(nickname, MESSAGE_TYPE_CONNECT);

		} else {
			throw new RuntimeException("Client not found.");
		}
	}

	/**
	 * Delete {@link Client} from list.
	 * 
	 * @param nickname
	 *            the nickname
	 * @throws Exception
	 *             if the {@link Client} is not found
	 * 
	 */
	private void deleteClientFromList(String nickname) {
		Client removedClient = getClientByNickname(nickname);

		if (!(removedClient == null)) {

			clientList.remove(removedClient);

		} else
			throw new RuntimeException("Client not found.");
	}

	/**
	 * Gets the {@link Client} by nickname.
	 * 
	 * @param nickname
	 *            the nickname
	 * @return the {@link Client}, or <b>null</b> if not found
	 */
	private Client getClientByNickname(String nickname) {
		assert nickname != null;
		Iterator<Client> iterator = clientList.iterator();

		boolean found = false;
		boolean ended = !iterator.hasNext();

		while (!found && !ended) {
			Client client = iterator.next();
			if (client.getNickname().equals(nickname)) {
				found = true;
				return client;
			} else {
				if (!iterator.hasNext())
					ended = true;
			}
		}
		return null;
	}

	/**
	 * Gets the {@link Client} index by nickname.
	 * 
	 * @param nickname
	 *            the nickname
	 * @return the {@link Server.Client} index
	 */
	private int getClientIndexByNickname(String nickname) {
		assert nickname != null;
		Iterator<Client> iterator = clientList.iterator();

		boolean found = false;
		boolean ended = !iterator.hasNext();

		while (!found && !ended) {
			Client client = iterator.next();
			if (client.getNickname().equals(nickname)) {
				found = true;
				return clientList.indexOf(client);
			} else {
				if (!iterator.hasNext())
					ended = true;
			}
		}
		return -1;
	}

	/**
	 * Check if {@link Client} exists in {@link Server.clientList}
	 * 
	 * @param nickname
	 *            the nickname
	 * @return true, if successful
	 */
	private boolean clientExists(String nickname) {
		assert nickname != null;
		Iterator<Client> iterator = clientList.iterator();

		boolean found = false;
		boolean ended = !iterator.hasNext();

		while (!found && !ended) {
			Client client = iterator.next();
			if (client.getNickname().equals(nickname)) {
				found = true;
				return found;
			} else {
				if (!iterator.hasNext())
					ended = true;
			}
		}
		return false;
	}

	/**
	 * Disconnect {@link Client}.
	 * 
	 * @param nickname
	 *            the nickname
	 * @throws Exception
	 *             if the {@link Server.Client} is not found
	 */
	private void disconnectUser(String nickname) throws Exception {
		assert (nickname != null);
		if (clientExists(nickname)) {
			processedMessages++;

			clientList.get(getClientIndexByNickname(nickname)).connected = false;
			onlineClients.remove(nickname);
			onlineUsers--;

			unfriendAll(nickname);

			floodMessageProcessor(nickname, MESSAGE_TYPE_DISCONNECT);

			deleteClientFromList(nickname);

		} else {
			throw new RuntimeException("Client not found.");
		}
	}

	/**
	 * Online name list.
	 * 
	 * @return as a string[] with the online {@link Client}, alphabetically
	 *         ordered
	 */
	private String[] onlineNameList() {

		String[] nameList = onlineClients.toArray(new String[0]);

		Arrays.sort(nameList);

		return nameList;
	}

	/**
	 * Name list output.
	 * 
	 * @return the output of a name list
	 */
	private String nameListOutput() {

		String output = "Online Users\n";

		String[] nameList = onlineNameList();

		for (int i = 0; i < nameList.length; i++) {

			output = output + (i + 1) + ": ";

			output = output + nameList[i];
			output = output + "\n";
		}

		output = output + "<End of User List>\n";

		return output;
	}

	/**
	 * Moodish message handler <br>
	 * After processing the sender and the contents, will relay the
	 * {@link moodish.comm.ServerSideMessage} to all clients following the
	 * sender
	 * 
	 * @param message
	 *            Recieves a {@link moodish.comm.ServerSideMessage} from
	 *            {@link moodish.comm.serverCommDummy} and handles it's content
	 * 
	 * 
	 */
	private void handleMoodishMessage(ServerSideMessage message) {

		assert (message != null);

		String from = message.getClientNickname();
		String mood = message.getPayload();

		assert (clientExists(from));
		assert (mood != null);

		LinkedList<String> followedBy = getClientByNickname(from).followedBy;
		processedMessages++;

		targetedMessageProcessor(from, mood, followedBy);

	}

	/**
	 * Friendship message handler <br>
	 * After processing the sender and the contents, will relay the
	 * {@link moodish.comm.ServerSideMessage} to the client now followed by the
	 * sender, and will update the client lists
	 * 
	 * @param message
	 *            Recieves a {@link moodish.comm.ServerSideMessage} from
	 *            {@link moodish.comm.serverCommDummy} and handles it's content
	 * 
	 * 
	 */
	private void handleFriendshipMessage(ServerMessage message) {
		assert (message != null);
		String newFriend = message.getPayload();
		String sender = message.getClientNickname();

		Client newFriendClient = getClientByNickname(newFriend);
		Client senderClient = getClientByNickname(sender);
		processedMessages++;

		if (!senderClient.isFollowerOf(newFriend))
			clientList.get(getClientIndexByNickname(sender)).addFollower(
					newFriend);
		else
			throw new RuntimeException(
					"Targetted client was already a friend of source client");
		if (!newFriendClient.isFollowedBy(sender)) {
			clientList.get(getClientIndexByNickname(newFriend)).addFollowedBy(
					sender);
		} else
			throw new RuntimeException(
					"Targetted client was already a friend of source client");

	}

	/**
	 * Unfriendship message handler <br>
	 * After processing the sender and the contents, will relay the
	 * {@link moodish.comm.ServerSideMessage} to the client now unfollowed by
	 * the sender, and will update the client lists
	 * 
	 * @param message
	 *            Recieves a {@link moodish.comm.ServerSideMessage} from
	 *            {@link moodish.comm.serverCommDummy} and handles it's content
	 * 
	 * 
	 */
	private void handleUnriendshipMessage(ServerSideMessage message) {
		assert (message != null);
		String newUnfriend = message.getPayload();
		String sender = message.getClientNickname();

		Client newUnfriendClient = getClientByNickname(newUnfriend);
		Client senderClient = getClientByNickname(sender);
		processedMessages++;

		assert (clientExists(newUnfriend) && clientExists(sender) && !senderClient
				.equals(newUnfriendClient));

		if (senderClient.isFollowerOf(newUnfriend)) {
			clientList.get(getClientIndexByNickname(sender)).removeFollowerOf(
					newUnfriend);
		} else
			throw new RuntimeException(
					"Targetted client was not a friend of source client.");

		if (newUnfriendClient.isFollowedBy(sender)) {
			clientList.get(getClientIndexByNickname(newUnfriend))
					.removeFollowedBy(sender);
		}

		else
			throw new RuntimeException(
					"Targetted client was not a friend of source client");

	}

	/**
	 * Unfriend all.
	 * 
	 * Removes all friends from a disconnecting client and informs them of it.
	 * 
	 * @param nickname
	 *            the nickname of the disconnecting {@link Client}
	 */
	private void unfriendAll(String nickname) {
		assert (clientExists(nickname));

		LinkedList<String> followerOf = getClientByNickname(nickname).followerOf;
		LinkedList<String> followedBy = getClientByNickname(nickname).followedBy;

		targetedMessageProcessor(nickname, MESSAGE_TYPE_UNFRIENDSHIP,
				followerOf);
		targetedMessageProcessor(nickname, MESSAGE_TYPE_UNFRIENDSHIP,
				followedBy);

	}

	/**
	 * Floods all online {@link Client} with a message
	 * 
	 * @param from
	 *            the sender of the message
	 * @param type
	 *            the message type
	 */
	private void floodMessageProcessor(String from, int type) {

		assert (clientExists(from) && ((0 >= type) && (type <= 4)));

		LinkedList<String> floodList = onlineClients;

		Iterator<String> iterator = floodList.iterator();

		boolean ended = !iterator.hasNext();

		switch (type) {
		default: {
			throw new RuntimeException(
					"Message Processor error, invalid arguments recieved");
		}
		case MESSAGE_TYPE_CONNECT: {

			while (!ended) {
				messageSender(from, iterator.next(), MESSAGE_TYPE_CONNECT);

				if (!iterator.hasNext())
					ended = true;
			}
			return;
		}
		case MESSAGE_TYPE_DISCONNECT: {
			while (!ended) {
				messageSender(from, iterator.next(), MESSAGE_TYPE_DISCONNECT);

				if (!iterator.hasNext())
					ended = true;
			}
			return;
		}
		}
	}

	/**
	 * Targeted message processor.
	 * 
	 * @param from
	 *            sender
	 * @param type
	 *            the {@link moodish.comm.ServerSideMessage} type
	 * @param targetClientNames
	 *            recieves a list with all the message destinations
	 */
	private void targetedMessageProcessor(String from, int type,
			LinkedList<String> targetClientNames) {

		Iterator<String> iterator = targetClientNames.iterator();
		boolean ended = !iterator.hasNext();

		if (getClientByNickname(from) != null) {

			switch (type) {
			default:
				throw new RuntimeException(
						"Targetted message processor error. Invalid arguments recieved");

			case MESSAGE_TYPE_UNFRIENDSHIP: {
				while (!ended) {
					messageSender(from, iterator.next(),
							MESSAGE_TYPE_UNFRIENDSHIP);

					if (!iterator.hasNext())
						ended = true;
				}
				return;
			}

			case MESSAGE_TYPE_MOODISH: {

				while (!ended) {

					if (!iterator.hasNext())
						ended = true;
				}
				return;
			}
			}
		}
	}

	/**
	 * Targeted message processor, for moodish messages.
	 * 
	 * @param from
	 *            the from
	 * @param mood
	 *            the mood
	 * @param targetClientNames
	 *            the target {@link Client} names
	 */
	private void targetedMessageProcessor(String from, String mood,
			LinkedList<String> targetClientNames) {
		assert (from != null && mood != null && targetClientNames != null && !from
				.equals(mood));

		Iterator<String> iterator = targetClientNames.iterator();

		boolean ended = !iterator.hasNext();

		if (getClientByNickname(from) != null) {

			while (!ended) {

				messageSender(from, iterator.next(), mood);

				if (!iterator.hasNext())
					ended = true;
			}

		} else
			throw new RuntimeException(
					"Moodish message processor error. Invalid arguments recieved");
	}

	/**
	 * Message sender. <br>
	 * This method will <b>NOT</b> execute should the server be stopped or the
	 * serverCommDummy buffer be empty
	 * 
	 * @param from
	 *            the sender
	 * @param to
	 *            the target
	 * @param type
	 *            the {@link server.comm.ServerSideMessage} type to be sent
	 */
	private void messageSender(String from, String to, int type) {
		processedMessages++;

		// The next line will prevent the sender to function in case the server
		// is stopped, or the buffer is not connected

		assert (!stopped && serverCommDummy != null);

		if (serverCommDummy == null || stopped)
			return;

		if (!(from == null) && !(to == null) && !(from == to)) {

			switch (type) {
			default:
				throw new RuntimeException(
						"Message Sender error. Invalid arguments received.");

			case MESSAGE_TYPE_CONNECT: {
				serverCommDummy.sendClientConnected(to, from);
				serverCommDummy.sendClientConnected(from, to);
				return;
			}

			case MESSAGE_TYPE_DISCONNECT: {
				serverCommDummy.sendClientDisconnected(to, from);
				return;
			}

			case MESSAGE_TYPE_FRIENDSHIP: {
				System.out.println("olaolaola");
				serverCommDummy.sendNewFriendship(from, to);

				return;
			}

			case MESSAGE_TYPE_UNFRIENDSHIP: {
				System.out.println("SOU ESTUPIDO SE VIER AQUIIII");
				serverCommDummy.sendNewUnfriendship(to, from);
				return;
			}
			}
		}
	}

	/**
	 * Message sender for moodish messages.
	 * 
	 * @param from
	 *            sender
	 * @param to
	 *            target
	 * @param mood
	 *            the mood
	 */
	private void messageSender(String from, String to, String mood) {
		processedMessages++;
		// The next line will prevent the sender to function in case the server
		// is stopped, or the buffer is not connected

		assert (!stopped && serverCommDummy != null);

		if (serverCommDummy == null || stopped)
			return;
		assert from != null && to != null && mood != null && !from.equals(to);
		serverCommDummy.sendMoodishMessage(from, to, mood);
	}
}

// #EOF